#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys, os, time
from datetime import datetime
import threading
from threading import Thread
import json
import csv

from loguru import logger

sys.path.append(os.getcwd())
from utils import app_config
from utils import serialport_com
from utils import call_acq_control_driver
from utils import voltageSerialCommand

from models.data_model import Probe, probe_schema, probes_schema

from webapi.matlab_engine import matlab_eng


class AcquisitionControl(object):
    __init_flag = False

    def __new__(cls, *args, **kwargs):
        if not hasattr(cls, '_instance'):
            cls._instance = super().__new__(cls, *args, **kwargs)
        return cls._instance

    def __init__(self):
        super().__init__()

    def initSystem(self):
        logger.debug("enter AcquisitionControl.initSystem() ")
        
        #获取可用的串口
        self.serialPortCom = serialport_com.SerialPortCom()
        self.comports = self.serialPortCom.port_check()
        # 3个串口
        self.mmscm_com_port = ""
        self.voltage_positive_com_port = ""
        self.voltage_negative_com_port = ""

        self.delay_time_us = 0

        ### 初始化采集控制模块，用于下发matlab生成的采集控制参数
        self.callAcqDriver = call_acq_control_driver.CallAcqDriverControl()

        self.person_name = ""
        self.subject_name = ""
        self.probe_name = ""
        self.probe_in_use = None

        ## default acquisition method is 
        self.acquisition_method_name = "single_angle_plane_wave"

        self.current_experiment_folder = ""
        self.current_acquisition_folder = ""

        self.global_cancel_flag = False
        self.acquisition_task = None
        self.acq_progress_bar = 0

        self.acq_mutex = threading.Lock()

        with open("./data/config/acquisition_config.json", 'r', encoding = 'utf-8') as file:
            self.acquisition_config = json.load(file)
            #print(self.acquisition_config)
        
        self.mmscm_com_port = self.acquisition_config["laser_serial_port"]
        self.voltage_positive_com_port = self.acquisition_config["positive_serial_port"]
        self.voltage_negative_com_port = self.acquisition_config["negative_serial_port"]

        self.__safetyGuatanteeWhenStart(self.mmscm_com_port)

        logger.debug("leave AcquisitionControl.initSystem() successfully.")

    def getComPorts(self):
        return self.comports
    
    def getDefaultComPorts(self):
        return {"laser_serial_port": self.mmscm_com_port,
                "positive_serial_port": self.voltage_positive_com_port,
                "negative_serial_port": self.voltage_negative_com_port}
    
    def getAllAcquisitionMethods(self):
        pai_acquisition_methods = {}
        for method in self.acquisition_config["acquisition_methods"]:
            pai_acquisition_methods[method["name"]] = method["display_name"]
        return pai_acquisition_methods

    def getAcquisitionMethodSetting(self, method_name):
        for method in self.acquisition_config["acquisition_methods"]:
            if method["name"] == method_name:
                self.setAcquisitionMethod(method_name)
                return method
            else:
                continue
        return None
    
    def setAcquisitionMethod(self, method_name):
        self.acquisition_method_name = method_name

    def setCurrentProbe(self, current_probe):
        self.probe_in_use = current_probe
    
    def getCurrentProbe(self):
        return self.probe_in_use

    ## For safety, close qswitch and close flash when software starts
    def __safetyGuatanteeWhenStart(self, port_name):
        if not self.openMMSCMComPort(port_name):
            return
        self.closeQSwitch()
        self.closeFlash()

    ## open voltage com port
    def __openVoltageComPort(self, port_name):
        baudrate = 9600
        bytesize = 8
        stopbits = 1
        parity = "N"
        return self.serialPortCom.port_open(port_name, baudrate, bytesize, stopbits, parity)
    
    ## set US voltage
    def setVoltage(self, positive_comport, nagative_comport, voltage):
        logger.debug("start setVoltage successfully. positive_comport:{0}, nagative_comport:{1}, voltage:{2}.", positive_comport, nagative_comport, voltage)
        self.voltage_positive_com_port = positive_comport
        self.voltage_negative_com_port = nagative_comport
        # open positive com
        status = self.__openVoltageComPort(positive_comport)
        # send data
        mode = "HEX"

        vo = float(voltage)   #  v
        vset = int((5 - (vo + 1.5) / 4.8) * 1000)       # mv
        vset = vset if vset < 5000 else 5000
        vset = vset if vset > 0 else 0

        voltage = int(vset)
        sendVoltageData = voltageSerialCommand.fun_Send_VoltageControl(voltage)
        send_com_data = " ".join(sendVoltageData)
        self.serialPortCom.data_send(mode, send_com_data)
        # close positive com
        self.serialPortCom.port_close()
        
        # open negative com
        status = self.__openVoltageComPort(nagative_comport)
        # send data
        self.serialPortCom.data_send(mode, send_com_data)
        # close negative com
        self.serialPortCom.port_close()
        logger.debug("end setVoltage successfully. ")
        return status

    #打开串口, 打开串口成功后 OpenFlash 使能
    #AT                      %测试串口通信及初始化:
    #AT+CH1+OUT+0            %通道1 关闭
    #AT+CH0+OUT+0            %通道0 关闭
    def openMMSCMComPort(self, portname):
        self.mmscm_com_port = portname
        baudrate = 115200
        bytesize = 8
        stopbits = 1
        parity = "N"
        if not self.serialPortCom.port_open(portname, baudrate, bytesize, stopbits, parity):
            logger.error("Port Error: 此设备串口不能被打开！")
            return False

        if not self.sendDataCheck("AT"):
            return False

        if not self.sendDataCheck( "AT+CH1+OUT+0"):
            return False

        if not self.sendDataCheck("AT+CH0+OUT+0"):
            return False
        logger.debug("MMSCM opens com port successfully. ")
        return True


    #开启Flash（等10s后才能开启Q-Switch）
    #AT+CH0+RT+0                    %通道0 上升沿起始延迟0ns
    #AT+CH0+WID+100000              %通道0 脉宽100000ns 100μs
    #AT+CH0+CYCLE+100000000         %通道0 周期100ms
    #AT+CH0+OUT+1                   %通道0 开启
    def openFlash(self):
        if not self.sendDataCheck("AT+CH0+RT+0"):
            return False
        
        if not self.sendDataCheck("AT+CH0+WID+100000"):
            return False
        
        if self.acquisition_method_name == "single_angle_plane_wave":
            if not self.sendDataCheck("AT+CH0+CYCLE+100000000"):
                return False
        elif self.acquisition_method_name == "single_shot_multiple_receive":
            if not self.sendDataCheck("AT+CH0+CYCLE+200000000"):
                return False
        ## TODO: 其他的采集模式是否要不同的值
        
        if not self.sendDataCheck("AT+CH0+OUT+1"):
            return False
        
        logger.debug("MMSCM opens flash successfully. ")
        return True
        

    #开启Q-Switch（开启Q-Switch后不能关闭Flash）
    #AT+CH1+RT+400000          %通道1 上升沿起始延迟400000ns（通道1与通道0默认同步，通道1上升沿相对通道0上升沿的典型延迟值：400000ns~260000ns）
    #AT+CH1+WID+100000         %通道1 脉宽100000ns 100μs
    #AT+CH1+CYCLE+100000000    %通道1 周期100ms
    #AT+CH1+OUT+1              %通道1 开启
    def openQSwitch(self, delay_time_us):
        self.delay_time_us = delay_time_us
        try:
            delaytime_ns = int(delay_time_us * 1000)
        except:
            logger.exception("set delay time invalidly")
            return False

        strATCH1RT = str("AT+CH1+RT+") + str(delaytime_ns)

        if not self.sendDataCheck(strATCH1RT):
            return False

        if not self.sendDataCheck("AT+CH1+WID+100000"):
            return False

        if not self.sendDataCheck("AT+CH1+CYCLE+100000000"):
            return False

        if not self.sendDataCheck("AT+CH1+OUT+1"):
            return False

        logger.debug("MMSCM opens QSwitch successfully. ")
        return True

    #关闭Q-Switch
    #AT+CH1+OUT+0                        %通道1 关闭
    def closeQSwitch(self):
        if not self.sendDataCheck("AT+CH1+OUT+0"):
            return False

        logger.debug("MMSCM closes QSwitch successfully. ")
        return True

    #关闭Flash
    #AT+CH0+OUT+0                        %通道0 关闭
    def closeFlash(self):
        if not self.sendDataCheck("AT+CH0+OUT+0"):
            return False

        logger.debug("MMSCM closes Flash successfully. ")
        return True

    def sendDataCheck(self, sendData):
        mode = "ASCII"

        send_data_len = self.serialPortCom.data_send(mode, sendData)
        if not((len(sendData) + 2) == send_data_len):
            logger.error("Port Error: 串口数据发送失败！发送数据为：{}" + sendData)
            return False

        time.sleep(0.05)
        
        receive_data =  self.serialPortCom.data_receive(mode)
        if receive_data.endswith("OK"):
            logger.error("Port Error: 串口数据接收异常：{}" + receive_data)
            return False

        return True
    
    # input: person_name, subject_name, probe_name, date_time
    # output: experiment数据的存放路径
    def generateExperimentFolder(self, person_name, subject_name, probe_name, date_time):
        # 添加实验文件路径
        appConfig = app_config.AppConfig()
        acqDataFolder = appConfig.getAcquisitionDataFolder()
        #print(new_experiment.timestamp)
        #person_name = person_name.encode("utf-8")
        timeString = date_time.strftime("%Y-%m-%d-%H-%M-%S")
        experimentFolder = "-".join([person_name, subject_name, probe_name, timeString])

        experimentDataFolder = acqDataFolder + experimentFolder
        os.makedirs(experimentDataFolder, exist_ok = True)
        return experimentDataFolder

    # input： 界面上的采集参数，所属的experiment的数据目录, 实验人， 实验对象， 使用的探头
    # acquisition_mode: single_acquisition, continuous_acquisition
    # acquisition_method: 零角度平面波，复合平面波，单发多收
    # output：acquisition目录
    def startAcquisition(self, acquisition_mode,
                         ext_trigger_select, 
                         acq_sample_num, 
                         tx2rx_delay_us,
                         ext_trigger_num,
                         ext_trigger_period,
                         trigger_delay,
                         line_number,
                         pulse_width,
                         pulse_pattern_code,
                         pattern_repeat_cycle_num,
                         acq_LNA_US,
                         acq_PGA_US,
                         acq_Attenuation_US,
                         acq_LNA_PA,
                         acq_PGA_PA,
                         acq_Attenuation_PA,
                         laser_wave_length,
                         experiment_folder,
                         person_name,
                         subject_name,
                         probe_name,
                         date_time,
                         acq_voltage,
                         acquisition_method):

        timefolder = date_time.strftime("%Y-%m-%d-%H-%M-%S")
        acqDataFolder = experiment_folder + "/" + timefolder
        os.makedirs(acqDataFolder, exist_ok = True)

        tx_delay_time = 0
        if acquisition_method == "multi_angle_compound_plane_wave":
            method_setting = self.getAcquisitionMethodSetting("multi_angle_compound_plane_wave")
            tx_delay_time = float(method_setting["setting"]["tx_delay_time"])
            #print(f"get multi_angle_compound_plane_wave tx_delay_time: ", tx_delay_time)

        ### 写参数入csv文件
        csv_headers = ["PersonName", 
                       "SubjectName", 
                       "ProbeName", 
                       "AcqDateTime", 
                       "AcquisitionMethod",
                       "AcquisitionMode",
                       "LaserWaveLength(nm)", 
                       "ExtTriggerSelect",
                       "SamplingFrequency(MHz)",
                       "SampleNumbers",
                       "TX2RXDelay(us)",
                       "ExtTriggerNum",
                       "ExtTriggerPeriod(ms)",
                       "TriggerDelay(us)",
                       "LineNumber",
                       "TXDelayTime",
                       "PulseWidth(ns)",
                       "PulsePatternCode",
                       "PatternRepeatCycleNum",
                       "ACQ_LNA_US(dB)",
                       "ACQ_PGA_US(dB)",
                       "ACQ_Attenuation_US(dB)",
                       "ACQ_LNA_PA(dB)",
                       "ACQ_PGA_PA(dB)",
                       "ACQ_Attenuation_PA(dB)",
                       "ACQ_Voltage(v)"]
        acq_parameter = [person_name,
                         subject_name,
                         probe_name,
                         date_time.strftime("%Y-%m-%d-%H-%M-%S"),
                         str(acquisition_method),
                         str(acquisition_mode),
                         str(laser_wave_length),
                         str(ext_trigger_select),
                         "40",  # TODO： 确认是否为常量，在配置文件中配置？
                         str(acq_sample_num),
                         str(tx2rx_delay_us),
                         str(ext_trigger_num),
                         str(ext_trigger_period),
                         str(trigger_delay),
                         str(line_number),
                         str(tx_delay_time),
                         str(pulse_width),
                         pulse_pattern_code,
                         str(pattern_repeat_cycle_num),
                         str(acq_LNA_US),
                         str(acq_PGA_US),
                         str(acq_Attenuation_US),
                         str(acq_LNA_PA),
                         str(acq_PGA_PA),
                         str(acq_Attenuation_PA),
                         str(acq_voltage)]

        acq_parameter_csv = acqDataFolder + "/acquisition_parameter.csv"
        with open(acq_parameter_csv, "w+", encoding="utf-8", newline="") as f:
            writer = csv.writer(f)
            writer.writerow(csv_headers)
            writer.writerow(acq_parameter)

        appConfig = app_config.AppConfig()
        matlabSimModEN = appConfig.getMatlabSimulationMode()

        self.GBL_FolderPath = appConfig.getACQCmdScriptFolder()
        if not os.path.exists(self.GBL_FolderPath):
            logger.warning("{0} does not exist!", self.GBL_FolderPath)
            return

        pulseCounter = round(float(pulse_width) / 5 - 1)
        pulse_counter = float(pulseCounter if pulseCounter > 0 else 0)   

        matlab_scripts_acquisition = os.getcwd() + "/matlab_scripts/acquisition"
        matlab_eng.cd(matlab_scripts_acquisition)

        logger.debug('''call matlab function:fun_GenUltrasoundCmdScript." 
                    GBL_FolderPath:{0}, matlabSimModEN:{1}, ext_trigger_select:{2}, 
                    acq_sample_num:{3}, tx2rx_delay_us:{4}, ext_trigger_num:{5},
                    ext_trigger_period:{6}, trigger_delay:{7}, line_number:{8},
                    pulse_counter:{9}, pulse_pattern_code:{10}, pattern_repeat_cycle_num:{11},
                    acq_LNA_PA:{12}, acq_PGA_PA:{13}, acq_Attenuation_PA:{14},
                    acq_LNA_US:{15}, acq_PGA_US:{16}, acq_Attenuation_US:{17},
                    acquisition_method:{18}, tx_delay_time:{19}''', 
                    self.GBL_FolderPath, matlabSimModEN, ext_trigger_select,
                    acq_sample_num, tx2rx_delay_us, ext_trigger_num, 
                    ext_trigger_period, trigger_delay, line_number,
                    pulse_counter, pulse_pattern_code, pattern_repeat_cycle_num,
                    acq_LNA_PA, acq_PGA_PA, acq_Attenuation_PA,
                    acq_LNA_US, acq_PGA_US, acq_Attenuation_US,
                    acquisition_method, tx_delay_time)
        matlab_eng.fun_GenUltrasoundCmdScript(
            self.GBL_FolderPath,        # ACQCmdScriptFolder
            float(matlabSimModEN),      # Matlab Simulation Mode
            float(ext_trigger_select),  # external Trigger select 
            float(acq_sample_num) ,     # image depth 80mm
            float(tx2rx_delay_us) ,     # Transducer Thick, 1mm
            float(ext_trigger_num) ,      #100-External Trigger, 1-Minitus, ACQ = 4 * 48 = 192 Second, 3Minuts
            float(ext_trigger_period) * 1e-3 ,  #100ms
            float(trigger_delay) ,      #Trigger in term of us
            float(line_number) ,        #Constant
            float(pulse_counter),   # Number of Pulse in 5ns, example: 1, 5ns Pulse, DivPeriod = 2 * 1
            pulse_pattern_code,
            float(pattern_repeat_cycle_num),
            float(acq_LNA_PA),
            float(acq_PGA_PA),
            float(acq_Attenuation_PA),
            float(acq_LNA_US),
            float(acq_PGA_US),
            float(acq_Attenuation_US),
            str(acquisition_method),
            float(tx_delay_time))

        driverControlExePath = appConfig.getDriverControlExePath()
        acqCmdScriptPath = self.GBL_FolderPath + "REG_CMD_Line_Config.vh"

        # 启动一个下发控制命令的子进程，同时启动一个线程读取子进程的输出信息
        self.callAcqDriver.processOneAcq(driverControlExePath, acqCmdScriptPath, acqDataFolder)

        totalFileCount = ext_trigger_num + 1
        self.global_cancel_flag = False  #每次采集开始前重置flag, 重置进度条
        self.acq_progress_bar = 0  
        self.acquisition_task = Thread(target = self.__waitForAcquisition, args=[acquisition_mode, acqDataFolder, totalFileCount])
        self.acquisition_task.start()

        logger.debug("end startAcquisition successfully. It doesn't mean that data has been acquired successfully.")
        return acqDataFolder


    def __waitForAcquisition(self, acquisition_mode, acqDataFolder, totalFileCount):
        try:
            logger.debug("start __waitForAcquisition. acquisition_mode:{0}, total_bin_files_to_acquire:{1}", acquisition_mode, totalFileCount)
            if acquisition_mode == "single_acquisition":
                #单次采集模式下，采集文件达到指定的数目就停止采集
                while not self.global_cancel_flag:
                    time.sleep(0.01)
                    fileNumber = sum([os.path.isfile(os.path.join(acqDataFolder, listx)) for listx in os.listdir(acqDataFolder)])
                    if fileNumber < totalFileCount:
                        #进度条显示采集进度
                        self.acq_mutex.acquire()
                        self.acq_progress_bar = int((float)(fileNumber)/totalFileCount * 100)
                        self.acq_mutex.release()
                        continue
                    else:
                        # self.acq_mutex.acquire()
                        # self.acq_progress_bar = 100
                        # self.acq_mutex.release()
                        break
            elif acquisition_mode == "continuous_acquisition":
                #连续采集模式下，只要没有取消采集就一直采
                while not self.global_cancel_flag:
                    time.sleep(0.01)

        except Exception as error:
            logger.error(error)
        finally:
            self.acq_mutex.acquire()
            self.callAcqDriver.endAcq()
            #For safety, close qswitch and close flash when the acquisition ends
            self.closeQSwitch()
            self.closeFlash()
            logger.debug("end __waitForAcquisition. one acquisition really ends gracefully and successfully.")
            #self.serialPortCom.port_close()
            self.acq_progress_bar = 100  # 前端收到进度条为100%的信息才开始下一次采集
            self.acq_mutex.release()

    def endAcquisition(self):

        if self.acquisition_task is not None:
            logger.debug("start endAcquisition. intended to log here.")
            self.acq_mutex.acquire()
            self.global_cancel_flag = True
            self.acq_mutex.release()

            self.acquisition_task.join()
            self.acq_progress_bar = 0
        
        logger.debug("end endAcquisition.")
    
    def getAcquisitionProgress(self):

        self.acq_mutex.acquire()
        acq_progress = self.acq_progress_bar
        self.acq_mutex.release()

        return acq_progress
    
    def shutdown(self):
        #pai_acq.terminate_runtime()
        logger.debug("start shutdown.")
        matlab_eng.quit()
        self.closeQSwitch()
        self.closeFlash()
        logger.debug("end shutdown.")
    
###########################################################################

if __name__ == '__main__':
# define a global single variable
    acq_control = AcquisitionControl()
    acq_control.initSystem()
    progress = acq_control.getAcquisitionProgress()
    print(f"acq_control.getAcquisitionProgress : ", progress)
    acq_control.shutdown()